Kotlin最佳项目实战——欧瑞天气App
Kotlin项目实战——欧瑞天气App
我们已经学习了绝大多数Kotlin的核心技术以及如何用Kotlin开发Android App,也编写过大量的程序,但还没有设计过一款完整的App,本章将满足读者的这个愿望,设计一款可以访问网络的Android App:欧瑞天气。
16.1 项目概述
这款App用于从服务端获取天气预报信息,并显示在窗口区域。这款App会首先列出省级及其所辖城市和县区信息,如图16-1所示。
▲图16-1 列出省级及其所辖城市和县区信息
当单击某个城市或县区名称时,会在窗口上显示该城市或县区的天气情况,如图16-2所示。
▲图16-2 显示天气情况
这款App使用前面章节介绍的UI技术、网络技术,并且使用Kotlin语言编写。其中有一些Library使用了Java编写,实际上,这款App是Kotlin和Java的结合体。
16.2 添加依赖
在App中使用了大量的第三方Library,如gson、okhttp3、glide等,这些Library需要在app/build.gradle文件中的dependencies部分指定,如下所示:
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
compile 'com.android.support:appcompat-v7:25.1.1'
testCompile 'junit:junit:4.12'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.8.1'
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
implementation 'com.github.bumptech.glide:glide:4.0.0-RC1'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
}
16.3 实现主窗口
主窗口类是MainActivity,这是该App第一个要启动的窗口。该窗口类的实现代码如下:
Kotlin代码(主窗口类)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
if (prefs.getString("weather", null) != null) {
val intent = Intent(this, WeatherActivity::class.java)
startActivity(intent)
finish()
}
}
}
我们可以看到,MainActivity类的实现代码并不复杂,其中利用SharedPreferences对象读取了配置信息weather,这个配置信息用于指明是否曾经查询过某个城市的天气,如果查询过,直接显示该城市的天气信息。这里面涉及一个WeatherActivity类,这是专门用于显示天气信息的窗口。
下面看一下MainActivity使用的布局文件(activity_main.xml)。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <fragment android:id="@+id/choose_area_fragment" android:name="com.oriweather.fragment.ChooseAreaFragment" android:layout_width="match_parent" android:layout_height="match_parent" /></FrameLayout>
在布局文件中,使用<fragment>标签引用了一个ChooseAreaFragment类,这是什么呢?实际上,Fragment是从Android 3.0开始加入的类,相当于一个透明的Panel,用于封装逻辑和UI,可以作为一个组件使用。ChooseAreaFragment的作用就是实现城市和县区列表,以便单击可以显示相应地区的天气情况。
16.4 显示地区列表
ChooseAreaFragment封装了显示地区列表的逻辑,但是只有ChooseAreaFragment类还不够,还需要很多辅助类来完成相应的工作。例如,地区列表是从服务端获取的JSON数据,因此,需要有相应的类来完成从网络上获取数据的工作,而且获取的是JSON格式的数据。因此,在使用这些数据之前,需要先将其转换为Kotlin类。本节除了实现ChooseAreaFragment类外,还会讲解如何实现这些辅助类。
16.4.1 描述城市信息的数据类
从服务端获取的地区信息有3个级别:省、市和县区。这3个级别分别需要一个数据类描述。
Kotlin代码(数据类)
// 描述省信息的数据类
data class Province(var id:Int = 0, var provinceName:String, var proinceCode:String)
// 描述市信息的数据类
data class City(var id:Int = 0, var cityName:String, var cityCode:String, var provinceCode:String)
// 描述县区信息的数据类
data class County(var id:Int = 0, var countyName:String, var countyCode:String, var cityCode:String)
16.4.2 处理JSON格式的城市列表信息
当JSON格式的数据从服务端获取后,需要对这些数据进行解析。这个工作是由Utility对象完成的。
Kotlin代码(解析JSON格式的数据)
object Utility {
// 解析和处理服务器返回的省级数据
fun handleProvinceResponse(response: String): List<Province> {
var provinces = mutableListOf<Province>()
if (!TextUtils.isEmpty(response)) {
try {
// 将JSON数组转换为Kotlin数组形式
val allProvinces = JSONArray(response)
// 对数组循环处理,每一次循环都会创建一个Province对象
for (i in 0..allProvinces.length() - 1) {
val provinceObject = allProvinces.getJSONObject(i)
val province = Province(provinceName =
provinceObject.getString("name"),proinceCode = provinceObje
ct.getString("id"))
provinces.add(provinces.size, province)
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
return provinces
}
// 解析和处理服务器返回的市级数据
fun handleCityResponse(response: String, provinceCode: String): List<City> {
var cities = mutableListOf<City>()
if (!TextUtils.isEmpty(response)) {
try {
val allCities = JSONArray(response)
for (i in 0..allCities.length() - 1) {
val cityObject = allCities.getJSONObject(i)
val city = City(cityName = cityObject.getString("name"),cityCode
= cityObject.getString("id"),provinceCode = provinceCode)
cities.add(city)
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
return cities
}
// 解析和处理服务器返回的县区级数据
fun handleCountyResponse(response: String, cityCode: String): List<County> {
var counties = mutableListOf<County>()
if (!TextUtils.isEmpty(response)) {
try {
val allCounties = JSONArray(response)
for (i in 0..allCounties.length() - 1) {
val countyObject = allCounties.getJSONObject(i)
val county = County(countyName = countyObject.getString("name"), countyCode = countyObject.getString("id"),cityCode = cityCode)
counties.add(county)
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
return counties
}
// 将返回的JSON数据解析成Weather实体类
fun handleWeatherResponse(response: String): Weather? {
try {
val jsonObject = JSONObject(response)
val jsonArray = jsonObject.getJSONArray("HeWeather")
val weatherContent = jsonArray.getJSONObject(0).toString()
return Gson().fromJson(weatherContent, Weather::class.java)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
在Utility对象中有4个方法,其中前3个方法用于分析省、市和县区级JSON格式数据,并将这些数据转换为相应的对象。第4个方法用于分析描述天气信息的JSON数据,而且未使用Android SDK标准的API进行分析,而是使用了gson开源库对JSON数据进行分析,并返回一个Weather对象,Weather类与其他相关类的定义需要符合gson标准,这些内容会在下一节介绍。
16.4.3 天气信息描述类
为了演示Kotlin与Java混合开发,描述天气信息的类用Java编写。其中Weather是用于描述天气的信息的主类,还有一些相关的类一同描述整个天气信息,如Basic、AQI、Now等。总之,这些类是由服务端返回的JSON格式天气信息决定的。获取天气信息的URL格式如下:
https://geekori.com/api/weather/?id=weather_id
这里的weather_id就是地区编码,如沈阳市和平区的编码是210102。获取该地区天气信息的URL如下:
https://geekori.com/api/weather/?id=210102
Weather以及相关类的实现代码如下:
Java代码(Weather类)
public class Weather {
public String status;
public Basic basic;
public AQI aqi;
public Now now;
public Suggestion suggestion;
@SerializedName("daily_forecast")
public List<Forecast> forecastList;
}
Java代码(Basic类)
public class Basic {
@SerializedName("city")
public String cityName;
@SerializedName("id")
public String weatherId;
public Update update;
public class Update {
@SerializedName("loc")
public String updateTime;
}
}
Java代码(AQI类)
public class AQI {
public AQICity city;
public class AQICity {
public String aqi;
public String pm25;
}
}
Java代码(Now类)
public class Now
{
@SerializedName("tmp")
public String temperature;
@SerializedName("cond")
public More more;
public class More {
@SerializedName("txt")
public String info;
}
}
Java代码(Suggestion类)
public class Suggestion {
@SerializedName("comf")
public Comfort comfort;
@SerializedName("cw")
public CarWash carWash;
public Sport sport;
public class Comfort {
@SerializedName("txt")
public String info;
}
public class CarWash {
@SerializedName("txt")
public String info;
}
public class Sport {
@SerializedName("txt")
public String info;
}
}
16.4.4 获取城市信息的对象
如果在Java中,获取城市信息通常会使用静态方法,这样在任何地方都能调用。不过Kotlin中没有静态方法,取而代之的是对象,因此,为了封装这些功能,先要定义一个DataSupport对象。该对象主要封装了3个方法:getProvinces、getCities和getCounties。分别用于从服务端获取省、市和县区的信息。
获取省信息的URL如下:
在浏览器中查看这个URL指向的页面,会显示如下JSON格式的省信息。
[{"id":"110000","name":"北京市"},{"id":"120000","name":"天津市"},{"id":"130000", "name":"河北省"},{"id":"140000","name":"山西省"},{"id":"150000","name":"内蒙古自治区"},{"id":"210000","name":"辽宁省"},{"id":"220000","name":"吉林省"},{"id":"230000","name":"黑龙江省"},{"id":"310000","name":"上海市"},{"id":"320000","name":"江苏省"},{"id":"330000", "name":"浙江省"},{"id":"340000","name":"安徽省"},{"id":"350000","name":"福建省"},{"id": "360000","name":"江西省"},{"id":"370000","name":"山东省"},{"id":"410000","name":"河南省"},{"id":"420000","name":"湖北省"},{"id":"430000","name":"湖南省"},{"id":"440000","name":"广东省"},{"id":"450000","name":"广西壮族自治区"},{"id":"460000","name":"海南省"},{"id":"500000","name":"重庆市"},{"id":"510000","name":"四川省"},{"id":"520000","name":"贵州省"},{"id":"530000","name":"云南省"},{"id":"540000","name":"西藏自治区"},{"id":"610000","name":"陕西省"},{"id":"620000","name":"甘肃省"},{"id":"630000","name":"青海省"},{"id":"640000","name":"宁夏回族自治区"},{"id":"650000","name":"新疆维吾尔自治区"},{"id":"810000","name":"香港特别行政区"},{"id":"820000","name":"澳门特别行政区"}]
我们可以看到,这是一个JSON格式的数组,每一个数组元素是一个对象,表示一个省(直辖市、自治区或特别行政区)的信息,包括id和name,分别对应Province类的provinceCode和provinceName属性。
获取每一个省的城市列表的URL格式如下:
https://geekori.com/api/china/${provinceCode}
其中${provinceCode}表示省的代码,如辽宁省是210000。因此,获取辽宁省所有城市列表的URL如下:
https://geekori.com/api/china/210000
在浏览器中查看这个URL指向的页面,会显示如下内容。
[{"id":"210100","name":"沈阳市"},{"id":"210200","name":"大连市"},{"id":"210300","name": "鞍山市"},{"id":"210400","name":"抚顺市"},{"id":"210500","name":"本溪市"},{"id":"210600","name":"丹东市"},{"id":"210700","name":"锦州市"},{"id":"210800","name":"营口市"},{"id":"210900","name":"阜新市"},{"id":"211000","name":"辽阳市"},{"id":"211100","name":"盘锦市"},{"id":"211200","name":"铁岭市"},{"id":"211300","name":"朝阳市"},{"id":"211400","name":"葫芦岛市"}]
返回的仍然是JSON格式的数组,每一个数组元素是一个对象,对象的属性仍然有两个:id和name,分别对应City类的cityCode和cityName属性。
获取某一个城市的县区列表的URL格式如下:
https://geekori.com/api/china/{cityCode}
其中cityCode表示城市编码。例如,获取沈阳市所辖县区列表的URL如下:
https://geekori.com/api/china/210000/210100
在浏览器中查看这个URL指向的页面,会显示如下内容。
[{"id":"210101","name":"市辖区"},{"id":"210102","name":"和平区"},{"id":"210103","name":"沈河区"},{"id":"210104","name":"大东区"},{"id":"210105","name":"皇姑区"},{"id":"210106","name":"铁西区"},{"id":"210111","name":"苏家屯区"},{"id":"210112","name":"东陵区"},{"id":"210113","name":"新城子区"},{"id":"210114","name":"于洪区"},{"id":"210122","name":"辽中县"},{"id":"210123","name":"康平县"},{"id":"210124","name":"法库县"},{"id":"210181","name":"新民市"}]
现在我们已经了解了获取省、市和县区3级地区信息的URL格式,然后可以编写DataSupport类了,实现代码如下:
Kotlin代码(从服务端获取数据的对象)
由于篇幅文字:此处的代码已省略,在后台回复“kotlin” 下载代码
16.4.5 在ListView组件中显示地区列表
现在一切准备工作都完成了,接下来实现ChooseAreaFragment类,该类是Fragment的子类,用于显示地区列表。地区列表显示在一个ListView组件中。这个组件在与ChooseAreaFragment对应的布局文件choose_area.xml中定义,如下所示。
由于篇幅文字:此处的代码已省略,在后台回复“kotlin” 下载代码
在上面的布局文件中,除了定义一个ListView组件,还定义了一个TextView组件和一个Button组件,其中TextView组件用于显示当前列表上一级的文本,如当前列表是辽宁省中的市,那么这个TextView组件显示的是“辽宁省”。这个Button组件是一个回退按钮,单击可以回退到上一个级别。
下面我们简单说一下这个ListView组件,这个组件是Android SDK提供的一个列表组件。这个组件采用MVC模式管理数据,也就是数据和视图分离。在显示数据时,需要提供Adapter对象,这个在MVC中称为Controller,用于衔接数据和视图。ChooseAreaFragment使用ListView组件显示地区列表的原理就是首先显示省列表,然后单击某一个省,就会重新设置数据源,显示当前省中的所有市,显示县区列表也类似。
下面看一下ChooseAreaFragment类的完整实现。
Kotlin代码(显示地区列表)
由于篇幅文字:此处的代码已省略,在后台回复“kotlin” 下载代码
在上面的代码中,调用了以前实现的DataSupport对象中的相应方法获取省、市和县区列表,并利用Adapter显示在ListView组件中。
16.5 显示天气信息
最后需要实现的就是在WeatherActivity中显示天气信息。在获取地区信息时使用的是HttpURLConnection,而这次,我们使用OkHttp组件来获取天气信息。在HttpUtil对象中封装了一个sendOkHttpRequest方法,用于通过OkHttp从服务端获取数据。
Kotlin代码(通过OkHttp从服务端获取数据)
object HttpUtil
{
fun sendOkHttpRequest(address: String, callback: okhttp3.Callback)
{
val client = OkHttpClient()
val request = Request.Builder().url(address).build()
client.newCall(request).enqueue(callback)
}
}
下面先看一下WeatherActivity使用的布局文件(activity_weather.xml)。
由于篇幅文字:此处的代码已省略,在后台回复“kotlin” 下载代码
在这段布局文件中,最后放置了一个<fragment>,引用了在前面实现的ChooseArea Fragment。可能我们还记得,在前面讲activity_main.xml时,也通过<fragment>标签引用了ChooseAreaFragment,那么这是怎么回事呢?ChooseAreaFragment为什么被引用了两次呢?其实,ChooseAreaFragment被重用了两次。第一次是在activity_main.xml中,当第一次运行App时,还没有显示过任何地区的天气信息,那么ChooseAreaFragment是全屏显示在主窗口(MainActivity)上。第二次是在activity_weather.xml中,我们看到,包括<fragment>在内,所有的组件都放在了DrawerLayout中。这是一个抽屉布局,也就是可以像抽屉一样拉出和显示,移动版QQ就有这样的效果,读者可以去体验。放在抽屉布局中的ChooseAreaFragment会随着抽屉的拉开而显示,随着抽屉的关闭而隐藏。
最后,我们看一下WeatherActivity类的完整实现代码。
Kotlin代码(显示指定地区的天气信息)
由于篇幅文字:此处的代码已省略,在后台回复“kotlin” 下载代码
在WeatherActivity中使用了一个SwipeRefreshLayout类,这是用于显示刷新效果的布局。当显示天气信息后,向下滑动窗口,会显示如图16-3所示的刷新效果。松开后,会重新加载当前页面。
16.6 小结
本章实现了一个Android App,尽管这个App不算大,但完全可以演示使用Kotlin开发Android App的完整过程。本章实现的App综合使用了UI、Activity、布局、网络等技术。希望读者根据本书提供的Demo源代码以及本书讲解的知识独立完成这个项目,这样会让自己的Android和Kotlin开发功力有大幅度提升。
Kotlin入门,本文摘自《Kotlin程序开发入门精要》
一本全程视频陪伴贴心跟踪初学者学习效果的Kotlin开发书
(点击图片查看详情)
编辑推荐
1
基于最新的Kotlin版本
2
本书分为三部分:Kotlin基础、Android开发和项目实战,读者通过对本书的学习,不仅可以掌握Kotlin的知识,还可以将Kotlin用于项目实战。
3
赠送近70小时视频课程,包括Kotlin和Android,供初学者学习使用
4
通过极客题库提供大量的测试题以及过关斩将系统,读者可以用通关的方式更有效地学习书中的知识
5
随书赠送价值300元优惠卡,可以用于购买李宁老师其他视频课程以及现在及未来提供的其他服务
6
有问题的学员可以通过欧瑞科技(创始人是李宁)的问答社区区(https://geekori.com)提问,会有专人回答读者提出的问题,以及通过博客系统总结书中学到的知识,同时,提出问题、回答问题以及撰写的博客越多,就会增加读者的积分,以后学习李宁老师其他的课程和获取其他资源,会有更大的优惠。
仅仅一本书怎么行?海量视频、海量习题快速入门
延伸推荐
点击关键词阅读更多新书:
Python|机器学习|Kotlin|Java|移动开发|机器人|有奖活动|Web前端|书单
点击阅读原文,购买《Kotlin程序开发入门精要》